#!/usr/bin/env python
# encoding: utf-8
"""
comando.py

Created by Martin Schiøth Dyrby on 2012-03-17.
Copyright (c) 2012 __MyCompanyName__. All rights reserved.
"""

import utils.simplejson as json
from utils.config import Config
import re
import base64
from sets import Set
from google.appengine.ext.db import Query
from google.appengine.api.datastore_types import Key

class Comando:
	instance = None
	ENDPOINTS = "endpoints"
	RESOLVE_EXPRESSION = "resolveExpression"
	
	def setService(self, serviceName, commandName, responseType, requestType, restriction, restricted, params):
		self.services[serviceName] = {
        	'type': commandName,
        	'response': responseType,
        	'request': requestType,
        	'restriction': restriction,
        	'restricted': restricted,
        	'params': params
		}

	def init(self, request, response):
		Comando.instance = self
		self.request = request
		self.response = response
		self._request_params = {}
		arguments = request.arguments()
		for argument in arguments:
			self._request_params[argument] = request.get(argument)
		
		self.services = {}
		f = file('config.comando.cfg')
		self.cfg = Config(f)
		
		""" SETUP SERVICES """
		tmpcommands = self.cfg.commands
		for command_name in tmpcommands.iterkeys():
			command_data = tmpcommands[command_name]

			command_type = command_data['type']

			try:
				command_response = command_data['response']
			except AttributeError:
				command_response = 'json'
				
			try:
				command_request = command_data['request']
			except AttributeError:
				command_request = 'request'

			try:
				command_restriction = command_data['restriction']
			except AttributeError:
				command_restriction = ''

			try:
				command_restricted = command_data['restricted']
			except AttributeError:
				command_restricted = ''

			try:
				command_params = command_data['params']
			except AttributeError:
				command_params = ''
			
			self.setService(command_name, command_type, command_response, command_request, command_restriction, command_restricted, command_params)

		self.setService(Comando.ENDPOINTS, 'comando.CreateAltEndpointsDocument', 'raw', 'request', '', '', {})
		self.setService(Comando.RESOLVE_EXPRESSION, 'comando.ResolveExpression', 'json', 'script', '', '', {})

		""" SETUP EXPRESSIONS """
		self.expressions = self.cfg.expression

		""" SETUP LOGGING """
		if self.cfg.logging is not None:
			self.logging = self.cfg.logging
		else:
			self.logging = None
		
		
		""" EXECUTE INITS """
		tmpinits = self.cfg.init
		for init_command_name in tmpinits:
			self.doExecute(init_command_name, 'script', self._request_params, True)
	
	def execute(self, commandName, request):
		return self.doExecute(commandName, 'script', request, True)
	
	def display(self, serviceName, method):
		return self.doExecute(serviceName, method, self._request_params, True)
	
	def doExecute(self, commandName, method, initRequest, doLog):
		if self.services.has_key(commandName) == False: return None
		
		service = self.services[commandName]

		command_class_name = service['type']
		response_type = service['response'].lower()
		request_type = service['request'].lower()
		restriction = service['restriction'].lower()
		restricted = service['restricted']
		params = service['params']
		
		method = method.lower()
		
		if initRequest is None:
			request = self._request_params
		else: 
			request = initRequest
		
		if commandName == Comando.ENDPOINTS:
			request['services'] = self.services

		if restriction != '':
			# resolve restriction
			restrictionResult = self.runCommand("comando.ResolveExpression", {ResolveExpression.EXPRESSION: self.expressions[restriction]})
			proceed = restrictionResult.status() == 1
			if proceed == False and restricted != '':
				return self.doExecute(restricted, "script", request, doLog)
		else:
			proceed = True

		if proceed == False:
			result = ComandoResult()
			result.setResponseType(response_type)
			result.setError("Service '"+commandName+"' is restricted")
			return result
			
		if method != 'script':
			if request_type != 'request':
				if request_type != method:
					result = ComandoResult()
					result.setResponseType(response_type)
					result.setError("Service '"+commandName+"' is not accessible through request type '" + method + "'")
					return result
		
		for param in params:
			request[param] = params[param]

		
		result = self.runCommand(command_class_name, request)
		result.setResponseType(response_type)
		
		if doLog == True:
			self.runCommand(self.logging, {'command': commandName + " ("+command_class_name+")", 'request': request})
		
		return result
		
	def runCommand(self, command_class_name, request):
		modulepart = command_class_name.split('.')
		command_class_name = modulepart.pop()
		module = __import__(".".join(modulepart), globals(), locals(), [command_class_name])
		command_clazz = getattr(module, command_class_name)
		command = command_clazz()
		command.init(request)
		return command.execute()
	
	def runAuthCommand(self, command_class_name, request):
		modulepart = command_class_name.split('.')
		command_class_name = modulepart.pop()
		module = __import__(".".join(modulepart), globals(), locals(), [command_class_name])
		command_clazz = getattr(module, command_class_name)
		command = command_clazz(self.request,self.response)
		command.init(request)
		return command.execute()
		
class ComandoResult:
	def __init__(self):

		self._status = 0
		self._data = None
		self._error = None
		self._responseType = 'json'
		self._output = ''
		self._location = ''
	
	def status(self):
		return self._status
	
	def data(self, key):
		if self._data is not None:
			return self._data[key]
		return None
	
	def setStatus(self, status):
		self._status = status
	
	def setError(self, error):
		self._status = 0
		self._error = error
	
	def setData(self, key, value):
		self._status = 1
		if self._data is None:
			self._data = {}
		
		self._data[key] = value
	
	def setOutput(self, value):
		self._output = value
	
	def setResponseType(self, value):
		self._responseType = value
	
	def setLocation(self, value):
		self._location = value
	
	def location(self):
		return self._location
	
	def hasLocation(self):
		return self._location != '' and self._location is not None
	
	def response(self):
		if self._responseType == 'json':
			if self._status == 1:
				if self._data is None:
					return '{"status":1}'
				else:
					return '{"status":1, "data": '+json.dumps(self._data, default=self.encode)+'}'
			else:
				return '{"status":0, "error": '+json.dumps(self._error, default=self.encode)+'}'
		elif self._responseType == 'xml':
			return '<comando><status>'+str(self._status)+'</status></comando>'
		elif self._responseType == 'raw':
			if self._output == '':
				if self._status == 0:
					if self._error is not None:
						return 'Error: '+self._error
					else:
						return 'Error'
				else:
					return 'success'
			else:	
				return self._output
	
	def encode(self, o):
		if isinstance(o, Set):
			objlist = []
			for key in o:
				objlist.append(key)
			return objlist
		
		elif isinstance(o, Query):
			objlist = []
			for obj in o:
				objlist.append(obj)
			return objlist
		
		elif isinstance(o, Key):
			return str(o)
		
		else:
			obj = o
			try:
				return o.to_obj()
			except:
				pass
			
		raise TypeError(repr(o) + " is not JSON serializable")


class AbstractValidationCommand:
	def init(self, request):
		self.params = {}
		
		required_fields = self.required()
		self.is_valid = True

		for fieldName in required_fields:
			if request.has_key(fieldName):
				self.params[fieldName] = request[fieldName]
			else:
				self.params[fieldName] = None
				self.is_valid = False
		
		optional_fields = self.optional()

		for fieldName in optional_fields:
			try:
				if request[fieldName] is not None:
					self.params[fieldName] = request[fieldName]
				else:
					self.params[fieldName] = None
			except KeyError:
				pass
	
	def execute(self):
		if self.is_valid:
			return self.doExecute()

		result = ComandoResult()
		result.setError('Missing parameters in request.')
		return result
	
	def doExecute(self):
		pass
	
	def required(self):
		return {}
	
	def optional(self):
		return {}
	
	def getParam(self, paramName):
		try:
			return self.params[paramName]
		except KeyError:
			return None

class AbstractRestrictionCommand(object):
	def __init__(self, request, response):
		self.request = request
		self.response = response
	
	def init(self, request):
		pass
	def execute(self):
		pass

class AbstractHttpAuthenticationCommand(AbstractRestrictionCommand):
	def execute(self):
		if not 'Authorization' in self.request.headers:
			self.response.headers['WWW-Authenticate'] = 'Basic realm=""'
			self.response.set_status(401)
			self.response.out.write("Authorization required")
		else:
			auth = self.request.headers['Authorization']
			(username, password) = base64.b64decode(auth.split(' ')[1]).split(':')
			self.username = username
			self.password = password
			return self.doExecute()
		return ComandoResult()

class CreateAltEndpointsDocument(AbstractValidationCommand):
	def required(self):
		return frozenset(['services'])
	def optional(self):
		return frozenset([])
	def doExecute(self):
		comando = Comando.instance
		result = ComandoResult()
		
		formatted_services = {}
		p_endpoint = re.compile('^"""\s*Endpoint$')
		p_description = re.compile('^\s*Description$')
		p_request = re.compile('^\s*Request$')
		p_response_data = re.compile('^\s*Response data$')
		p_response_errors = re.compile('^\s*Response errors$')
		
		services = self.getParam('services')
		
		for serviceName in services:
			if serviceName == Comando.ENDPOINTS or serviceName == Comando.RESOLVE_EXPRESSION:
				continue

			service = services[serviceName]
			
			""" for each service read file """
			command_class_name = service['type']
			command_class_name_part = command_class_name.split('.')
			command_name = command_class_name_part.pop()
			command_path = "/".join(command_class_name_part)

			formatted_services[serviceName] = {'class_contents': '', 'description': '', 'request': [], 'data': [], 'errors': []}

			try:
				f = open("../"+command_path+".py", 'r')
			except IOError:
				continue
			
			contents = f.read();

			class_index = contents.find('class '+command_name)
			if class_index == -1:
				continue
			
			class_contents = contents[:class_index]
			prev_class_index = class_contents.rfind('class')
			description_index = class_contents.rfind('Description')
			request_index = class_contents.rfind('Request')
			response_data_index = class_contents.rfind('Response data')
			response_errors_index = class_contents.rfind('Response errors')
			
			if description_index > -1 and description_index > prev_class_index:
				description_contents = class_contents[description_index+11:request_index]
				request_contents = class_contents[request_index+7:response_data_index]
				response_data_contents = class_contents[response_data_index+13:response_errors_index]
				response_errors_contents = class_contents[response_errors_index+15:]
			else:
				description_contents = ''
				request_contents = ''
				response_data_contents = ''
				response_errors_contents = ''
			

			formatted_services[serviceName]['description'] = description_contents
			requests = request_contents.splitlines()
			formatted_services[serviceName]['request'] = requests
			response_data = response_data_contents.splitlines()
			formatted_services[serviceName]['data'] = response_data
			response_errors = response_errors_contents.splitlines()
			formatted_services[serviceName]['errors'] = response_errors
			formatted_services[serviceName]['class_contents'] = request_contents

		
		""" add services to output """
		
		endpoints_index = comando.request.url.find(Comando.ENDPOINTS)
		baseurl = comando.request.url[0:endpoints_index]
		# body D5CA98
		# h1 D5CA98
		# h2
		# h3
		# header C72546
		# subheader 66424C
		# subheader2 768A4F
		# subheader3 B3C262
		# separator 768A4F
		
		output = "<html>\
	<head>\
		<title>Endpoints</title>\
		<style type=\"text/css\">\
			body {\
				font-family: Arial, sans-serif;\
				margin: 0;\
				padding: 0;\
				background-color: #D0C7AB;\
			}\
			.content {}\
			h1 {\
				font-size: 36;\
				color: #C56212\
			}\
			h2 {\
				font-size: 18;\
				color: #22384D\
			}\
			h3 {\
				font-size: 14;\
				color: #22384D\
			}\
			p {\
				font-size: 12;\
				color: #000\
			}\
			ul {\
				list-style-type: square;\
				font-size: 12;\
				color: #000\
			}\
			.header {\
				width: 100%;\
				height: 49;\
				padding: 10px;\
				background-color: #D0C7AB;\
			}\
			.separator { margin-top: 15px; width: 100%; height: 5px; background-color: #C56212; }\
			.subheader { width: 100%; height: 5px; background-color: #C56212; }\
			.subheader2 { width: 100%; height: 6px; background-color: #768A4F; }\
			.subheader3 { width: 100%; height: 8px; background-color: #22384D; }\
			.description { float: left; padding-right: 20px; width: 300px;}\
			.clear { clear: both; }\
			.service { padding: 10px; }\
		</style>\
	</head>\
	<body>\
		<div class=\"header\"><h1>Endpoints</h1></div>\
		<div class=\"subheader\"></div>\
		<div class=\"subheader2\"></div>\
		<div class=\"subheader3\"></div>\
		<div class=\"content\">\
		"
		for serviceName in formatted_services:
			output += '<div class="service">'
			output += '<h2>' + baseurl + serviceName+'</h2>'
#			output += '<p>'+formatted_services[serviceName]['class_contents']+'</p>'
			output += '<div class="description">'
			output += '<h3>Description</h3>'
			output += '<p>'+formatted_services[serviceName]['description']+'</p>'
			output += '</div>'

			output += '<div class="description">'
			output += '<h3>Request parameters</h3>'
			output += '<ul>'
			for param in formatted_services[serviceName]['request']:
				if param.strip() != '':
					output += '<li>'+param+'</li>' 
			output += '</ul>'
			output += '</div>'
			
			output += '<div class="description">'
			output += '<h3>Response data</h3>'
			output += '<ul>'
			for param in formatted_services[serviceName]['data']:
				if param.strip() != '':
					output += '<li>'+param+'</li>' 
			output += '</ul>'
			output += '</div>'

			output += '<div class="description">'
			output += '<h3>Response errors</h3>'
			output += '<ul>'
			for param in formatted_services[serviceName]['errors']:
				if param.strip() != '' and param.strip() != '"""':
					output += '<li>'+param+'</li>' 
			output += '</ul>'
			output += '</div>'
			output += '</div>'
			output += '<div class="clear"></div>'
			output += '<div class="separator"></div>'

		output += "</div></body></html>"
		
		result.setOutput(output)
			
		return result

class ResolveExpression():
	EXPRESSION = 'expression'
	def init(self, request):
		self.orgexpression = request[ResolveExpression.EXPRESSION]
	def execute(self):
		result = ComandoResult()
		value = self.doExecute(self.orgexpression)
		if value == True:
			result.setStatus(1)
		else:
			result.setStatus(0)
		return result
	def doExecute(self, expression):
		endIndex=0
		while endIndex > -1:
			endIndex=expression.find(")")
			if endIndex > -1:
				beginIndex = expression[0:endIndex].rfind('(')
				if beginIndex > -1:
					subexpression = expression[beginIndex+1:endIndex]
					subresult = self.doExecute(subexpression)
					newexpression = expression[0:beginIndex]+str(subresult)+expression[endIndex+1:]						
					expression = newexpression
				else:
					return False
		
		expression = self.resolveVars(expression)
		value = eval(expression)

		return value
	def resolveVars(self, expression):
		comando = Comando.instance
		varname = ""
		index=-1
		for c in expression:
			resolveVar = False
			index+=1
			try:
				int(c)
				resolveVar = True
			except ValueError:
				if c == " " or c == "=" or c == "*" or c == "/" or c == "+" or c == "-" or c == ">" or c == "<" or (varname == "" and c == "."):
					resolveVar = True
				else:
					varname += c
			
			if resolveVar:
				if varname == "" or varname == "True" or varname == "False" or varname == "or" or varname == "and":
					varname = ""
				else:
					parts = varname.split('.')
					commandname = parts[0]
					commandparam = parts[1]
					varresult = comando.execute(commandname,None)
					varvalue = varresult.data(commandparam)
					expression = expression[0:index-len(parts[0])-1-len(parts[1])]+str(varvalue)+expression[index:]
					return self.resolveVars(expression)
		return expression